'use strict';

import CanceledError from './CanceledError.js';

/**
 * A `CancelToken` is an object that can be used to request cancellation of an operation.
 *
 * @param {Function} executor The executor function.
 *
 * @returns {CancelToken}
 */
class CancelToken {
    constructor(executor) {
        if (typeof executor !== 'function') {
            throw new TypeError('executor must be a function.');
        }

        let resolvePromise;

        this.promise = new Promise(function promiseExecutor(resolve) {
            resolvePromise = resolve;
        });

        const token = this;

        // eslint-disable-next-line func-names
        this.promise.then(cancel => {
            if (!token._listeners) return;

            let i = token._listeners.length;

            while (i-- > 0) {
                token._listeners[i](cancel);
            }
            token._listeners = null;
        });

        // eslint-disable-next-line func-names
        this.promise.then = onfulfilled => {
            let _resolve;
            // eslint-disable-next-line func-names
            const promise = new Promise(resolve => {
                token.subscribe(resolve);
                _resolve = resolve;
            }).then(onfulfilled);

            promise.cancel = function reject() {
                token.unsubscribe(_resolve);
            };

            return promise;
        };

        executor(function cancel(message, config, request) {
            if (token.reason) {
                // Cancellation has already been requested
                return;
            }

            token.reason = new CanceledError(message, config, request);
            resolvePromise(token.reason);
        });
    }

    /**
     * Throws a `CanceledError` if cancellation has been requested.
     */
    throwIfRequested() {
        if (this.reason) {
            throw this.reason;
        }
    }

    /**
     * Subscribe to the cancel signal
     */

    subscribe(listener) {
        if (this.reason) {
            listener(this.reason);
            return;
        }

        if (this._listeners) {
            this._listeners.push(listener);
        } else {
            this._listeners = [listener];
        }
    }

    /**
     * Unsubscribe from the cancel signal
     */

    unsubscribe(listener) {
        if (!this._listeners) {
            return;
        }
        const index = this._listeners.indexOf(listener);
        if (index !== -1) {
            this._listeners.splice(index, 1);
        }
    }

    /**
     * Returns an object that contains a new `CancelToken` and a function that, when called,
     * cancels the `CancelToken`.
     */
    static source() {
        let cancel;
        const token = new CancelToken(function executor(c) {
            cancel = c;
        });
        return {
            token,
            cancel
        };
    }
}

export default CancelToken;
